/*
 * Copyright 2013-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.kiki.stack.http.feign.SpringMvcContract.loadbalancer;

import io.kiki.stack.http.feign.Client;
import io.kiki.stack.http.feign.Request;
import io.kiki.stack.http.feign.Response;
import lombok.Getter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.*;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import static io.kiki.stack.http.feign.SpringMvcContract.loadbalancer.LoadBalancerUtils.buildRequestData;
import static io.kiki.stack.http.feign.SpringMvcContract.loadbalancer.LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing;

/**
 * A {@link Client} implementation that uses {@link LoadBalancerClient} to select a
 * {@link ServiceInstance} to use while resolving the request host.
 */
@SuppressWarnings({"unchecked", "rawtypes"})
public class FeignBlockingLoadBalancerClient implements Client {

    private static final Log LOG = LogFactory.getLog(FeignBlockingLoadBalancerClient.class);

    // Visible for Sleuth instrumentation
    @Getter
    private final Client delegate;

    private final LoadBalancerClient loadBalancerClient;

    private final LoadBalancerClientFactory loadBalancerClientFactory;

    private final List<LoadBalancerFeignRequestTransformer> loadBalancerFeignRequestTransformerList;

    /**
     * @deprecated in favour of
     * {@link FeignBlockingLoadBalancerClient#FeignBlockingLoadBalancerClient(Client, LoadBalancerClient, LoadBalancerClientFactory, List)}
     */
    @Deprecated(forRemoval = true)
    public FeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient, LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
        this.delegate = delegate;
        this.loadBalancerClient = loadBalancerClient;
        this.loadBalancerClientFactory = loadBalancerClientFactory;
        this.loadBalancerFeignRequestTransformerList = Collections.emptyList();
    }

    /**
     * @deprecated in favour of
     * {@link FeignBlockingLoadBalancerClient#FeignBlockingLoadBalancerClient(Client, LoadBalancerClient, LoadBalancerClientFactory, List)}
     */
    @Deprecated(forRemoval = true)
    public FeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient, LoadBalancerClientFactory loadBalancerClientFactory) {
        this.delegate = delegate;
        this.loadBalancerClient = loadBalancerClient;
        this.loadBalancerClientFactory = loadBalancerClientFactory;
        this.loadBalancerFeignRequestTransformerList = Collections.emptyList();
    }

    public FeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient, LoadBalancerClientFactory loadBalancerClientFactory, List<LoadBalancerFeignRequestTransformer> loadBalancerFeignRequestTransformerList) {
        this.delegate = delegate;
        this.loadBalancerClient = loadBalancerClient;
        this.loadBalancerClientFactory = loadBalancerClientFactory;
        this.loadBalancerFeignRequestTransformerList = loadBalancerFeignRequestTransformerList;
    }

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        final URI originalUri = URI.create(request.url());
        String serviceId = originalUri.getHost();
        Assert.state(serviceId != null, "Request URI does not contain a valid hostname: " + originalUri);
        String hint = getHint(serviceId);
        DefaultRequest<RequestDataContext> defaultRequest = new DefaultRequest<>(new RequestDataContext(buildRequestData(request), hint));
        Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator.getSupportedLifecycleProcessors(loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class), RequestDataContext.class, ResponseData.class, ServiceInstance.class);
        supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(defaultRequest));
        ServiceInstance serviceInstance = loadBalancerClient.choose(serviceId, defaultRequest);
        org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> defaultResponse = new DefaultResponse(serviceInstance);
        if (serviceInstance == null) {
            String message = "Load balancer does not contain an instance for the service " + serviceId;
            if (LOG.isWarnEnabled()) {
                LOG.warn(message);
            }
            supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(CompletionContext.Status.DISCARD, defaultRequest, defaultResponse)));
            return Response.builder().request(request).status(HttpStatus.SERVICE_UNAVAILABLE.value()).body(message, StandardCharsets.UTF_8).build();
        }
        String reconstructedUrl = loadBalancerClient.reconstructURI(serviceInstance, originalUri).toString();
        Request newRequest = buildRequest(request, reconstructedUrl, serviceInstance);
        return executeWithLoadBalancerLifecycleProcessing(delegate, options, newRequest, defaultRequest, defaultResponse, supportedLifecycleProcessors);
    }

    protected Request buildRequest(Request request, String reconstructedUrl) {
        return Request.create(request.httpMethod(), reconstructedUrl, request.headers(), request.body(), request.charset(), request.requestTemplate());
    }

    protected Request buildRequest(Request request, String reconstructedUrl, ServiceInstance instance) {
        Request newRequest = buildRequest(request, reconstructedUrl);
        if (loadBalancerFeignRequestTransformerList != null) {
            for (LoadBalancerFeignRequestTransformer transformer : loadBalancerFeignRequestTransformerList) {
                newRequest = transformer.transformRequest(newRequest, instance);
            }
        }
        return newRequest;
    }

    private String getHint(String serviceId) {
        LoadBalancerProperties loadBalancerProperties = loadBalancerClientFactory.getProperties(serviceId);
        String defaultHint = loadBalancerProperties.getHint().getOrDefault("default", "default");
        String hintPropertyValue = loadBalancerProperties.getHint().get(serviceId);
        return hintPropertyValue != null ? hintPropertyValue : defaultHint;
    }

}
